{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "81a6f12e",
   "metadata": {},
   "source": [
    "下面几段代码展示朴素贝叶斯模型的训练和预测。这里使用的数据集为本书自制的Books数据集，包含约1万本图书的标题，分为3种主题。首先是预处理，针对文本分类的预处理主要包含以下步骤：\n",
    "\n",
    "- 通常可以将英文文本全部转换为小写，或者将中文内容全部转换为简体，等等，这一般不会改变文本内容。\n",
    "- 去除标点。英文中的标点符号和单词之间没有空格（如——“Hi, there!”），如果不去除标点，“Hi,”和“there!”会被识别为不同于“Hi”和“there”的两个词，这显然是不合理的。对于中文，移除标点一般也不会影响文本的内容。\n",
    "- 分词。中文汉字之间没有空格分隔，中文分词有时比英文分词更加困难，此处不再赘述。\n",
    "- 去除停用词（如“I”、“is”、“的”等）。这些词往往大量出现但没有具体含义。\n",
    "- 建立词表。通常会忽略语料库中频率非常低的词。\n",
    "- 将词转换为词表索引（ID），便于机器学习模型使用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "5936ceb0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "train size = 8627 , test size = 2157\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|█████████████████████████████████████████████████████████████████████████████| 8627/8627 [01:04<00:00, 132.86it/s]\n",
      "100%|█████████████████████████████████████████████████████████████████████████████| 2157/2157 [00:16<00:00, 131.96it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['python', '编程', '入门', '教程']\n",
      "{'计算机类': 0, '艺术传媒类': 1, '经管类': 2}\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "import json\n",
    "import os\n",
    "import requests\n",
    "import re\n",
    "from tqdm import tqdm\n",
    "from collections import defaultdict\n",
    "from string import punctuation\n",
    "import spacy\n",
    "from spacy.lang.zh.stop_words import STOP_WORDS\n",
    "nlp = spacy.load('zh_core_web_sm')\n",
    "\n",
    "\n",
    "class BooksDataset:\n",
    "    def __init__(self):\n",
    "        train_file, test_file = 'train.jsonl', 'test.jsonl'\n",
    "\n",
    "        # 下载数据为JSON格式，转化为Python对象\n",
    "        def read_file(file_name):\n",
    "            with open(file_name, 'r', encoding='utf-8') as fin:\n",
    "                json_list = list(fin)\n",
    "            data_split = []\n",
    "            for json_str in json_list:\n",
    "                data_split.append(json.loads(json_str))\n",
    "            return data_split\n",
    "\n",
    "        self.train_data, self.test_data = read_file(train_file),\\\n",
    "            read_file(test_file)\n",
    "        print('train size =', len(self.train_data), \n",
    "              ', test size =', len(self.test_data))\n",
    "        \n",
    "        # 建立文本标签和数字标签的映射\n",
    "        self.label2id, self.id2label = {}, {}\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in data_split:\n",
    "                txt = data['class']\n",
    "                if txt not in self.label2id:\n",
    "                    idx = len(self.label2id)\n",
    "                    self.label2id[txt] = idx\n",
    "                    self.id2label[idx] = txt\n",
    "                label_id = self.label2id[txt]\n",
    "                data['label'] = label_id\n",
    "\n",
    "    def tokenize(self, attr='book'):\n",
    "        # 使用以下两行命令安装spacy用于中文分词\n",
    "        # pip install -U spacy\n",
    "        # python -m spacy download zh_core_web_sm\n",
    "        # 去除文本中的符号和停用词\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in tqdm(data_split):\n",
    "                # 转为小写\n",
    "                text = data[attr].lower()\n",
    "                # 符号替换为空\n",
    "                tokens = [t.text for t in nlp(text) \\\n",
    "                    if t.text not in STOP_WORDS]\n",
    "                # 这一步比较耗时，因此把tokenize的结果储存起来\n",
    "                data['tokens'] = tokens\n",
    "\n",
    "    # 根据分词结果建立词表，忽略部分低频词，\n",
    "    # 可以设置词最短长度和词表最大大小\n",
    "    def build_vocab(self, min_freq=3, min_len=2, max_size=None):\n",
    "        frequency = defaultdict(int)\n",
    "        for data in self.train_data:\n",
    "            tokens = data['tokens']\n",
    "            for token in tokens:\n",
    "                frequency[token] += 1 \n",
    "\n",
    "        print(f'unique tokens = {len(frequency)}, '+\\\n",
    "              f'total counts = {sum(frequency.values())}, '+\\\n",
    "              f'max freq = {max(frequency.values())}, '+\\\n",
    "              f'min freq = {min(frequency.values())}')    \n",
    "\n",
    "        self.token2id = {}\n",
    "        self.id2token = {}\n",
    "        total_count = 0\n",
    "        for token, freq in sorted(frequency.items(),\\\n",
    "            key=lambda x: -x[1]):\n",
    "            if max_size and len(self.token2id) >= max_size:\n",
    "                break\n",
    "            if freq > min_freq:\n",
    "                if (min_len is None) or (min_len and \\\n",
    "                    len(token) >= min_len):\n",
    "                    self.token2id[token] = len(self.token2id)\n",
    "                    self.id2token[len(self.id2token)] = token\n",
    "                    total_count += freq\n",
    "            else:\n",
    "                break\n",
    "        print(f'min_freq = {min_freq}, min_len = {min_len}, '+\\\n",
    "              f'max_size = {max_size}, '\n",
    "              f'remaining tokens = {len(self.token2id)}, '\n",
    "              f'in-vocab rate = {total_count / sum(frequency.values())}')\n",
    "\n",
    "    # 将分词后的结果转化为数字索引\n",
    "    def convert_tokens_to_ids(self):\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in data_split:\n",
    "                data['token_ids'] = []\n",
    "                for token in data['tokens']:\n",
    "                    if token in self.token2id:\n",
    "                        data['token_ids'].append(self.token2id[token])\n",
    "\n",
    "        \n",
    "dataset = BooksDataset()\n",
    "dataset.tokenize()\n",
    "print(dataset.train_data[0]['tokens'])\n",
    "print(dataset.label2id)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26d79e05",
   "metadata": {},
   "source": [
    "完成分词后，对出现次数超过3次的词元建立词表，并将分词后的文档转化为词元id的序列。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "0d6b1918",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "unique tokens = 6956, total counts = 54884, max freq = 1635, min freq = 1\n",
      "min_freq = 3, min_len = 2, max_size = None, remaining tokens = 1650, in-vocab rate = 0.7944209605713869\n",
      "[18, 26, 5, 0]\n"
     ]
    }
   ],
   "source": [
    "dataset.build_vocab(min_freq=3)\n",
    "dataset.convert_tokens_to_ids()\n",
    "print(dataset.train_data[0]['token_ids'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d096d95f",
   "metadata": {},
   "source": [
    "接下来将数据和标签准备成便于训练的矩阵格式。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "ba632265",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "train_X, train_Y = [], []\n",
    "test_X, test_Y = [], []\n",
    "\n",
    "for data in dataset.train_data:\n",
    "    x = np.zeros(len(dataset.token2id), dtype=np.int32)\n",
    "    for token_id in data['token_ids']:\n",
    "        x[token_id] += 1\n",
    "    train_X.append(x)\n",
    "    train_Y.append(data['label'])\n",
    "for data in dataset.test_data:\n",
    "    x = np.zeros(len(dataset.token2id), dtype=np.int32)\n",
    "    for token_id in data['token_ids']:\n",
    "        x[token_id] += 1\n",
    "    test_X.append(x)\n",
    "    test_Y.append(data['label'])\n",
    "train_X, train_Y = np.array(train_X), np.array(train_Y)\n",
    "test_X, test_Y = np.array(test_X), np.array(test_Y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3938acdb",
   "metadata": {},
   "source": [
    "下面代码展示朴素贝叶斯的训练和预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "f13251b7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "P(计算机类) = 0.4453460067230787\n",
      "P(艺术传媒类) = 0.26660484525327466\n",
      "P(经管类) = 0.2880491480236467\n",
      "P(教程|计算机类) = 0.5726495726495726\n",
      "P(基础|计算机类) = 0.6503006012024048\n",
      "P(设计|计算机类) = 0.606694560669456\n",
      "test example-0, prediction = 0, label = 0\n",
      "test example-1, prediction = 0, label = 0\n",
      "test example-2, prediction = 1, label = 1\n",
      "test example-3, prediction = 1, label = 1\n",
      "test example-4, prediction = 1, label = 1\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "\n",
    "class NaiveBayes:\n",
    "    def __init__(self, num_classes, vocab_size):\n",
    "        self.num_classes = num_classes\n",
    "        self.vocab_size = vocab_size\n",
    "        self.prior = np.zeros(num_classes, dtype=np.float64)\n",
    "        self.likelihood = np.zeros((num_classes, vocab_size),\\\n",
    "            dtype=np.float64)\n",
    "        \n",
    "    def fit(self, X, Y):\n",
    "        # NaiveBayes的训练主要涉及先验概率和似然的估计，\n",
    "        # 这两者都可以通过计数简单获得\n",
    "        for x, y in zip(X, Y):\n",
    "            self.prior[y] += 1\n",
    "            for token_id in x:\n",
    "                self.likelihood[y, token_id] += 1\n",
    "                \n",
    "        self.prior /= self.prior.sum()\n",
    "        # laplace平滑\n",
    "        self.likelihood += 1\n",
    "        self.likelihood /= self.likelihood.sum(axis=0)\n",
    "        # 为了避免精度溢出，使用对数概率\n",
    "        self.prior = np.log(self.prior)\n",
    "        self.likelihood = np.log(self.likelihood)\n",
    "    \n",
    "    def predict(self, X):\n",
    "        # 算出各个类别的先验概率与似然的乘积，找出最大的作为分类结果\n",
    "        preds = []\n",
    "        for x in X:\n",
    "            p = np.zeros(self.num_classes, dtype=np.float64)\n",
    "            for i in range(self.num_classes):\n",
    "                p[i] += self.prior[i]\n",
    "                for token in x:\n",
    "                    p[i] += self.likelihood[i, token]\n",
    "            preds.append(np.argmax(p))\n",
    "        return preds\n",
    "\n",
    "nb = NaiveBayes(len(dataset.label2id), len(dataset.token2id))\n",
    "train_X, train_Y = [], []\n",
    "for data in dataset.train_data:\n",
    "    train_X.append(data['token_ids'])\n",
    "    train_Y.append(data['label'])\n",
    "nb.fit(train_X, train_Y)\n",
    "\n",
    "for i in range(3):\n",
    "    print(f'P({dataset.id2label[i]}) = {np.exp(nb.prior[i])}')\n",
    "for i in range(3):\n",
    "    print(f'P({dataset.id2token[i]}|{dataset.id2label[0]}) = '+\\\n",
    "          f'{np.exp(nb.likelihood[0, i])}')\n",
    "\n",
    "test_X, test_Y = [], []\n",
    "for data in dataset.test_data:\n",
    "    test_X.append(data['token_ids'])\n",
    "    test_Y.append(data['label'])\n",
    "    \n",
    "NB_preds = nb.predict(test_X)\n",
    "    \n",
    "for i, (p, y) in enumerate(zip(NB_preds, test_Y)):\n",
    "    if i >= 5:\n",
    "        break\n",
    "    print(f'test example-{i}, prediction = {p}, label = {y}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1cf6399",
   "metadata": {},
   "source": [
    "下面使用第3章介绍的TF-IDF方法得到文档的特征向量，并使用PyTorch实现逻辑斯谛回归模型的训练和预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "21a3bc79",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import sys\n",
    " \n",
    "# 使用绝对路径添加模块目录到 sys.path\n",
    "module_path = r'D:\\Tools\\JupyterData\\@Hands-on-NLP-main\\code'\n",
    "if module_path not in sys.path:\n",
    "    sys.path.append(module_path)\n",
    "\n",
    "# 导入 my_utils 模块\n",
    "try:\n",
    "    from my_utils import TFIDF\n",
    "except ModuleNotFoundError as e:\n",
    "    print(f\"Module not found: {e}\")\n",
    "         \n",
    "tfidf = TFIDF(len(dataset.token2id))\n",
    "tfidf.fit(train_X)\n",
    "train_F = tfidf.transform(train_X)\n",
    "test_F = tfidf.transform(test_X)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc8af30b",
   "metadata": {},
   "source": [
    "逻辑斯谛回归可以看作一个一层的神经网络模型，使用PyTorch实现可以方便地利用自动求导功能。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "1ddebf0c",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "epoch-49, loss=0.1330: 100%|█| 50/50 [00:10<00:00,  4.83it/s\n"
     ]
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAGwCAYAAABB4NqyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAABZqklEQVR4nO3deVwU9f8H8Ndy36CCHIqI94F4gAd4H+GdZaWppZZWZmamZR6VppV2mZVpp/rttvLIXx6J5n2LmCh4goIKIoicAsLO7w9k3WXvZXdnj9fz8eDRzsxnZt87TuybzykRBEEAERERkR1xEDsAIiIiInNjAkRERER2hwkQERER2R0mQERERGR3mAARERGR3WECRERERHaHCRARERHZHSexAzA3qVSKGzduwNvbGxKJROxwiIiISAeCIKCwsBAhISFwcKh9/Y3dJUA3btxAaGio2GEQERGRATIyMtCwYcNaX8fuEiBvb28AVTfQx8dH5GiIiIhIFwUFBQgNDZV9j9eW3SVA1c1ePj4+TICIiIisjLG6r7ATNBEREdkdJkBERERkd5gAERERkd1hAkRERER2hwkQERER2R0mQERERGR3mAARERGR3WECRERERHaHCRARERHZHSZAREREZHeYABEREZHdYQJEREREdocJkJFUSgXcLChFWk6x2KEQERGRFkyAjCQz/y66vr8Lg5bvEzsUIiIi0oIJkJF4uzoDAMoqpKwFIiIisnBMgIzE09VR9rrvx3vEC4SIiIi0YgJkJE6OvJVERETWgt/aRvT2sDay16ev3REvECIiItKICZARPR0TJnv98IqDIkZCREREmjABMiLnGs1g6bklIkVCREREmjABMrJZD7WQvd5/6ZaIkRAREZE6TICM7OX+zWWv5288I2IkREREpA4TICIiIrI7TIBM7GZBqdghEBERUQ1MgEygQ6if7PXbf7EZjIiIyNIwATKBr56Kkr0uLK0QMRIiIiJShQmQCQT5uqFHM38AgI+bs8jREBERUU1MgEzk0Y4NAADbz2ah3yd7kM2+QERERBaDCZCJ9G1VX/Y69VYxnvj6sIjREBERkTwmQCZS19NFYfsqZ4UmIiKyGEyATMjfy1Vhm81gREREloEJkAlNkFscFQAWbD4rUiREREQkjwmQCT3Xq4nC9t4LXBuMiIjIEjABMiE3Z0eF7ZLySlRUSkWK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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "eval_loss = 0.2868\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "from torch import nn\n",
    "\n",
    "class LR(nn.Module):\n",
    "    def __init__(self, input_dim, output_dim):\n",
    "        super(LR, self).__init__()\n",
    "        self.linear = nn.Linear(input_dim, output_dim)\n",
    "        \n",
    "    def forward(self, input_feats, labels=None):\n",
    "        outputs = self.linear(input_feats)\n",
    "        \n",
    "        if labels is not None:\n",
    "            loss_fc = nn.CrossEntropyLoss()\n",
    "            loss = loss_fc(outputs, labels)\n",
    "            return (loss, outputs)\n",
    "        \n",
    "        return outputs\n",
    "\n",
    "model = LR(len(dataset.token2id), len(dataset.label2id))\n",
    "\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "from torch.optim import SGD, Adam\n",
    "\n",
    "# 使用PyTorch的DataLoader来进行数据循环，因此按照PyTorch的接口\n",
    "# 实现myDataset和DataCollator两个类\n",
    "# myDataset是对特征向量和标签的简单封装便于对齐接口，\n",
    "# DataCollator用于批量将数据转化为PyTorch支持的张量类型\n",
    "class myDataset(Dataset):\n",
    "    def __init__(self, X, Y):\n",
    "        self.X = X\n",
    "        self.Y = Y\n",
    "        \n",
    "    def __len__(self):\n",
    "        return len(self.X)\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        return (self.X[idx], self.Y[idx])\n",
    "\n",
    "class DataCollator:\n",
    "    @classmethod\n",
    "    def collate_batch(cls, batch):\n",
    "        feats, labels = [], []\n",
    "        for x, y in batch:\n",
    "            feats.append(x)\n",
    "            labels.append(y)\n",
    "        # 直接将一个ndarray的列表转化为张量是非常慢的，\n",
    "        # 所以需要提前将列表转化为一整个ndarray\n",
    "        feats = torch.tensor(np.array(feats), dtype=torch.float)\n",
    "        labels = torch.tensor(np.array(labels), dtype=torch.long)\n",
    "        return {'input_feats': feats, 'labels': labels}\n",
    "\n",
    "# 设置训练超参数和优化器，模型初始化\n",
    "epochs = 50\n",
    "batch_size = 128\n",
    "learning_rate = 1e-3\n",
    "weight_decay = 0\n",
    "\n",
    "train_dataset = myDataset(train_F, train_Y)\n",
    "test_dataset = myDataset(test_F, test_Y)\n",
    "\n",
    "data_collator = DataCollator()\n",
    "train_dataloader = DataLoader(train_dataset, batch_size=batch_size,\\\n",
    "    shuffle=True, collate_fn=data_collator.collate_batch)\n",
    "test_dataloader = DataLoader(test_dataset, batch_size=batch_size,\\\n",
    "    shuffle=False, collate_fn=data_collator.collate_batch)\n",
    "optimizer = Adam(model.parameters(), lr=learning_rate,\\\n",
    "    weight_decay=weight_decay)\n",
    "model.zero_grad()\n",
    "model.train()\n",
    "\n",
    "from tqdm import tqdm, trange\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# 模型训练\n",
    "with trange(epochs, desc='epoch', ncols=60) as pbar:\n",
    "    epoch_loss = []\n",
    "    for epoch in pbar:\n",
    "        model.train()\n",
    "        for step, batch in enumerate(train_dataloader):\n",
    "            loss = model(**batch)[0]\n",
    "            pbar.set_description(f'epoch-{epoch}, loss={loss.item():.4f}')\n",
    "            loss.backward()\n",
    "            optimizer.step()\n",
    "            model.zero_grad()\n",
    "            epoch_loss.append(loss.item())\n",
    "\n",
    "    epoch_loss = np.array(epoch_loss)\n",
    "    # 打印损失曲线\n",
    "    plt.plot(range(len(epoch_loss)), epoch_loss)\n",
    "    plt.xlabel('training epoch')\n",
    "    plt.ylabel('loss')\n",
    "    plt.show()\n",
    "    \n",
    "    model.eval()\n",
    "    with torch.no_grad():\n",
    "        loss_terms = []\n",
    "        for batch in test_dataloader:\n",
    "            loss = model(**batch)[0]\n",
    "            loss_terms.append(loss.item())\n",
    "        print(f'eval_loss = {np.mean(loss_terms):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10808854",
   "metadata": {},
   "source": [
    "下面的代码使用训练好的模型对测试集进行预测，并报告分类结果。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "11a9bf62",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "test example-0, prediction = 0, label = 0\n",
      "test example-1, prediction = 0, label = 0\n",
      "test example-2, prediction = 1, label = 1\n",
      "test example-3, prediction = 1, label = 1\n",
      "test example-4, prediction = 1, label = 1\n"
     ]
    }
   ],
   "source": [
    "LR_preds = []\n",
    "model.eval()\n",
    "for batch in test_dataloader:\n",
    "    with torch.no_grad():\n",
    "        _, preds = model(**batch)\n",
    "        preds = np.argmax(preds, axis=1)\n",
    "        LR_preds.extend(preds)\n",
    "            \n",
    "for i, (p, y) in enumerate(zip(LR_preds, test_Y)):\n",
    "    if i >= 5:\n",
    "        break\n",
    "    print(f'test example-{i}, prediction = {p}, label = {y}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c5feb65e",
   "metadata": {},
   "source": [
    "下面的代码展示多分类情况下宏平均和微平均的算法。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "a5ac32c5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "NB: micro-f1 = 0.8961520630505331, macro-f1 = 0.8948572078813896\n",
      "LR: micro-f1 = 0.9151599443671766, macro-f1 = 0.9143871512947528\n"
     ]
    }
   ],
   "source": [
    "test_Y = np.array(test_Y)\n",
    "NB_preds = np.array(NB_preds)\n",
    "LR_preds = np.array(LR_preds)\n",
    "\n",
    "def micro_f1(preds, labels):\n",
    "    TP = np.sum(preds == labels)\n",
    "    FN = FP = 0\n",
    "    for i in range(len(dataset.label2id)):\n",
    "        FN += np.sum((preds == i) & (labels != i))\n",
    "        FP += np.sum((preds != i) & (labels == i))\n",
    "    precision = TP / (TP + FP)\n",
    "    recall = TP / (TP + FN)\n",
    "    f1 = 2 * precision * recall / (precision + recall)\n",
    "    return f1\n",
    "\n",
    "def macro_f1(preds, labels):\n",
    "    f_scores = []\n",
    "    for i in range(len(dataset.label2id)):\n",
    "        TP = np.sum((preds == i) & (labels == i))\n",
    "        FN = np.sum((preds == i) & (labels != i))\n",
    "        FP = np.sum((preds != i) & (labels == i))\n",
    "        precision = TP / (TP + FP)\n",
    "        recall = TP / (TP + FN)\n",
    "        f1 = 2 * precision * recall / (precision + recall)\n",
    "        f_scores.append(f1)\n",
    "    return np.mean(f_scores)\n",
    "\n",
    "print(f'NB: micro-f1 = {micro_f1(NB_preds, test_Y)}, '+\\\n",
    "      f'macro-f1 = {macro_f1(NB_preds, test_Y)}')\n",
    "print(f'LR: micro-f1 = {micro_f1(LR_preds, test_Y)}, '+\\\n",
    "      f'macro-f1 = {macro_f1(LR_preds, test_Y)}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ed554dc7-ec24-4bc6-86bd-a6210560d97f",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
